package server;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;

import net.ByteBuffer;
import net.Connection;
import net.Protocol;
import net.ServerInfo;
import net.TCPConnectionListener;
import net.UDPConnectionListener;
import util.Logger;

/**
 * Listens for and handles connections from other computers. Server ports used
 * are between 20,000 - 30,000
 * 
 * @author Jason
 * 
 */
public abstract class Server implements TCPConnectionListener,
		UDPConnectionListener, Protocol {

	protected final int tcpPort, udpPort;
	private ServerSocket tcpSocket;
	private DatagramSocket udpSocket;
	private Thread clientListener, udpPacketListener;
	private LinkedList<Connection> activeConnections;

	// maps the client's UDP socket address to its connection object
	private HashMap<ServerInfo, Connection> addressConnections;

	public Server(int tcpPort) {
		this(tcpPort, -1);
	}

	public Server(int tcpPort, int udpPort) {
		this.tcpPort = tcpPort;
		this.udpPort = udpPort;
	}

	private final Runnable clientListenerThread = new Runnable() {
		public void run() {
			while (clientListener != null) {
				try {
					Socket sock = tcpSocket.accept();
					Connection conn = new ServerSideConnection(Server.this,
							sock);
					activeConnections.add(conn);
					conn.addTCPConnectionListener(Server.this);
					conn.addUDPConnectionListener(Server.this);
					connectionReceived(conn);
				} catch (IOException e) {
					if (clientListener != null) {
						e.printStackTrace();
					}
					break;
				}
			}
		}
	};

	private final Runnable udpPacketListenerThread = new Runnable() {
		final byte[] buff = new byte[256];

		public void run() {
			DatagramPacket packet = new DatagramPacket(buff, buff.length);
			while (udpPacketListener != null) {
				try {
					udpSocket.receive(packet);
					ServerInfo info = new ServerInfo(packet.getAddress()
							.getHostAddress(), packet.getPort());
					Connection from = addressConnections.get(info);
					synchronized (addressConnections) {
						if (packet.getData()[0] == OPEN_UDP) {
							Logger.log(Logger.FINE, "Client sent"
									+ " UDP greeting.");
							if (from != null) {
								from.setUDPOutbound(info.port);
							}
							continue;
						}
					}
					handleUDPPacket(packet, from);
				} catch (IOException e) {
					if (udpPacketListener != null) {
						e.printStackTrace();
					}
					break;
				}
			}
		}
	};

	protected void handleUDPPacket(DatagramPacket packet, Connection from) {
		byte[] copy = Arrays.copyOfRange(packet.getData(), packet.getOffset(),
				packet.getOffset() + packet.getLength());
		from.udpReceived(copy);
	}

	/**
	 * Activates UDP with the given connection.
	 */
	public void activateUDP(Connection conn) {
		Logger.log(Logger.FINE, "Attempting to open UDP connection with "
				+ conn.getIP());
		ByteBuffer bb = new ByteBuffer(6);
		bb.add(DELIM.charAt(0));
		bb.add(OPEN_UDP);
		bb.addInt(udpPort);
		conn.sendTCP(bb.getBuffer());
	}

	private final DatagramPacket outboundPacket = new DatagramPacket(
			new byte[0], 0);

	public synchronized void sendUDP(byte[] bytes, int offset, int len,
			Connection to) {
		outboundPacket.setAddress(to.getAddress());
		outboundPacket.setPort(to.getUDPOutbound());
		outboundPacket.setData(bytes, offset, len);
		try {
			udpSocket.send(outboundPacket);
		} catch (Exception e) {
			Logger.log(e);
		}
	}

	public void startServer() throws IOException {
		if (clientListener != null) {
			System.err.println("The server is already started.");
			return;
		}
		tcpSocket = new ServerSocket(tcpPort);
		if (udpPort != -1) {
			udpSocket = new DatagramSocket(udpPort);
		}
		activeConnections = new LinkedList<Connection>();
		addressConnections = new HashMap<ServerInfo, Connection>();
		clientListener = new Thread(clientListenerThread);
		clientListener.start();
		if (udpSocket != null) {
			udpPacketListener = new Thread(udpPacketListenerThread);
			udpPacketListener.start();
		}
	}

	public void stopServer(boolean closeActiveConnections) {
		if (activeConnections == null
				|| (clientListener == null && !closeActiveConnections)) {
			Logger.log(Logger.WARNING, "The server is already stopped.");
			return;
		}

		Logger.log(Logger.FINE, "Stopping the server.");

		clientListener = null;
		if (closeActiveConnections) {
			LinkedList<Connection> temp = activeConnections;
			activeConnections = null;
			addressConnections = null;
			try {
				tcpSocket.close();
				if (closeActiveConnections) {
					for (Connection conn : temp) {
						if (conn != null)
							conn.exit();
					}
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	protected abstract void connectionReceived(Connection conn);

	public void tcpConnectionMade(Connection conn) {
	}

	public void tcpConnectionBroken(Connection conn) {
		if (activeConnections != null) {
			activeConnections.remove(conn);
		}
		if (addressConnections != null) {
			addressConnections.remove(new ServerInfo(conn.getIP(), conn
					.getUDPOutbound()));
		}
	}

	public void tcpMessageReceived(byte[] bytes, Connection conn) {

		if (bytes[0] == '!') {
			String msg = new String(bytes);
			String[] m = msg.split(DELIM);
			String command = m[0];
			if (command.equals(UDP_PORT)) {
				int port = Integer.valueOf(m[1]);
				ServerInfo info = new ServerInfo(conn.getIP(), port);
				synchronized (addressConnections) {
					addressConnections.put(info, conn);
					conn.setUDPOutbound(port);
				}
				Logger.log(Logger.FINE, "Client opened UDP.");
			}
		} else

			receive(bytes, conn);
	}

	public void udpConnectionMade(Connection conn) {
	}

	public void udpConnectionBroken(Connection conn) {
		if (activeConnections != null) {
			activeConnections.remove(conn);
		}
		if (addressConnections != null) {
			addressConnections.remove(new ServerInfo(conn.getIP(), conn
					.getUDPOutbound()));
		}
	}

	public void udpMessageReceived(byte[] bytes, Connection conn) {
		receive(bytes, conn);
	}

	protected abstract void receive(byte[] bytes, Connection from);

}
